Skip to content

feat: Replace Spring Dependency Management plugin with Gradle platform + lightweight BOM property overrides#15467

Open
jamesfredley wants to merge 12 commits into8.0.xfrom
feat/gradle-managed-version-overrides
Open

feat: Replace Spring Dependency Management plugin with Gradle platform + lightweight BOM property overrides#15467
jamesfredley wants to merge 12 commits into8.0.xfrom
feat/gradle-managed-version-overrides

Conversation

@jamesfredley
Copy link
Copy Markdown
Contributor

@jamesfredley jamesfredley commented Feb 26, 2026

Summary

This PR standardizes Grails on Gradle's native platform() dependency management - the modern, recommended approach - while preserving the one feature that Gradle platforms currently lack: property-based version overrides from BOMs (e.g. ext['slf4j.version'] = '2.0.9' or slf4j.version=2.0.9 in gradle.properties).

The Spring Dependency Management plugin served Grails well, but it duplicates significant Gradle functionality (dependency resolution, exclusions, BOM imports) and introduces behavioral differences from standard Gradle resolution. Gradle platforms now handle the vast majority of what the Spring DM plugin provided. The one gap is reading <properties> from BOM POMs and allowing project-level overrides (Gradle issue #9160 - confirmed "very unlikely" to be implemented).

This PR bridges that gap with a lightweight ~350-line utility (BomManagedVersions) so Grails can remove the Spring DM plugin entirely.

What This Changes

Core: BomManagedVersions.groovy (new)

A lightweight utility that:

  1. Resolves the BOM POM via a detached Gradle configuration (using the @pom artifact notation)
  2. Parses the POM XML using JDK's built-in DocumentBuilderFactory - no external dependencies. XML parsing is hardened (disallow-doctype-decl, external entity/DTD access disabled, setXIncludeAware(false))
  3. Extracts the <properties> block and <dependencyManagement> entries, building a mapping of property names to the artifacts they control
  4. Recursively follows <scope>import</scope> BOM imports (e.g., grails-bom imports spring-boot-dependencies). Interpolation is capped at MAX_PROPERTY_INTERPOLATION_DEPTH = 10 to prevent runaway recursion
  5. Applies overrides via Configuration.resolutionStrategy.eachDependency() - only for properties where project.hasProperty(name) is true (covering both ext['...'] and gradle.properties)

Plugin: GrailsGradlePlugin.groovy (modified)

  • Replaced applySpringDependencyManagementPlugin() with applyGrailsBom()
  • applyGrailsBom() adds project.dependencies.platform(bomCoordinates) to every declarable project configuration (mirroring the global behaviour that Spring DM provided via configurations.all() + resolutionStrategy.eachDependency()). Non-declarable configurations (apiElements, runtimeElements, etc.) inherit the constraints through their parent configurations.
  • Excludes tool and annotation-processor classpaths from the auto-applied platform, because platform() constraints participate in version conflict resolution and would otherwise upgrade transitive dependencies and break the tool:
    • Code-quality configurations: checkstyle, codenarc, pmd, spotbugs, spotbugsPlugins
    • Annotation-processor configurations: annotationProcessor and any *AnnotationProcessor (e.g. testAnnotationProcessor, integrationTestAnnotationProcessor). Micronaut projects import io.micronaut.platform:micronaut-platform on these configurations themselves; a second non-enforced grails-bom platform would let Gradle's highest-version conflict resolution upgrade javaparser-core, micronaut-inject-java, etc. beyond what the processor was compiled against.
  • Ensures the developmentOnly configuration is always available via configurations.maybeCreate('developmentOnly'), even when Spring Boot isn't applied or plugin ordering changes.
  • Calls BomManagedVersions.resolve() in an afterEvaluate block to enable property-based overrides.
  • validateEnforcedBom() (used by configureMicronaut()) now scans all grails-bom declarations on the implementation configuration and accepts the project as correctly configured if any one of them is an enforcedPlatform. This is required because applyGrailsBom() injects a regular platform(grails-bom) alongside the user's explicit enforcedPlatform(grails-bom); the previous first-match logic tripped on the plugin-injected one and rejected valid Micronaut projects.
  • Spring DM plugin is no longer applied or referenced.

Extension: GrailsExtension.groovy (modified)

  • The springDependencyManagement flag is deprecated with a message directing users to Gradle's native platform support
  • Flag is retained for backwards compatibility but has no effect

Build: plugins/build.gradle (modified)

  • Removed implementation 'io.spring.gradle:dependency-management-plugin' dependency

Examples & Docs

  • Removed io.spring.dependency-management plugin from example projects (gsp-spring-boot, graphql spring-boot-app)
  • Updated gradleDependencies.adoc to document the Gradle platform approach and property override mechanism
  • Updated comments in grailsCentralPublishing.gradle template and grails-bom/build.gradle

How Version Overrides Work

The mechanism is intentionally simple and mirrors exactly what the Spring DM plugin provided:

// In build.gradle
ext['slf4j.version'] = '2.0.9'

// Or in gradle.properties
slf4j.version=2.0.9

BomManagedVersions reads the BOM's <properties> to discover that slf4j.version controls org.slf4j:slf4j-api (and other slf4j artifacts), then uses eachDependency to force the override at resolution time.

Why Not Just Use Gradle Platforms?

Gradle platforms (platform()) handle:

  • BOM import and version alignment
  • Transitive dependency management
  • Dependency exclusions (via standard Gradle mechanisms)
  • Version constraints and conflict resolution

Gradle platforms do not handle reading <properties> from BOM POMs and allowing project-level overrides (Gradle issue #9160 - confirmed "very unlikely" to be implemented).

This is the specific gap BomManagedVersions fills.

Testing

  • Unit tests (BomManagedVersionsSpec): 4 tests covering POM parsing, property extraction, nested BOM imports, and property-to-artifact mapping
  • Functional test (BomPlatformFunctionalSpec): End-to-end Gradle TestKit test verifying that gradle.properties overrides are applied during dependency resolution
  • Verified the full CI matrix is green after merging the latest 8.0.x: Core/Forge/Gradle Plugin builds, all Functional / Hibernate5 / Mongodb functional test matrices (Java 21 & 25, indy=false & indy=true), Analyze (java), Validate Dependency Versions, rat-audit, Code Style, CodeQL

Commits

SHA Date Description
ca495ab 2026-02-26 feat: replace Spring Dependency Management plugin with Gradle platform + lightweight BOM property overrides - initial implementation of BomManagedVersions, switch from Spring DM to platform(), deprecate GrailsExtension.springDependencyManagement, update docs and example projects
531041ba 2026-02-26 Address review: add XInclude hardening and extract interpolation depth constant - setXIncludeAware(false) and extract MAX_PROPERTY_INTERPOLATION_DEPTH = 10
5e89656e 2026-02-26 fix: apply grails-bom platform to all declarable configurations - restore Spring DM's global behaviour by adding platform(grails-bom) to every declarable configuration; exclude code-quality tool classpaths
1a2cea3428a68177 2026-03-20 → 2026-04-16 seven Merge branch '8.0.x' into feat/gradle-managed-version-overrides commits keeping the branch in sync with 8.0.x (Spring Boot 4, Jackson 3, Java 25 Micronaut bumps, dependency-validator plugin, etc.). The final merge resolved one textual conflict in grails-data-graphql/examples/spring-boot-app/build.gradle (drop the Spring DM plugin while keeping org.apache.grails.buildsrc.dependency-validator, matching how other example build.gradles on 8.0.x look).
77932f2e 2026-04-16 fix: allow Micronaut BOM validator to coexist with plugin-injected platform - validateEnforcedBom() now scans all grails-bom declarations and passes if any one is enforcedPlatform, rather than failing on the first regular platform(grails-bom) the plugin injected
54fbe932 2026-04-16 fix: exclude annotation-processor configurations from grails-bom platform - do not add platform(grails-bom) to annotationProcessor / *AnnotationProcessor configurations; rename isCodeQualityConfiguration to isExcludedFromBomPlatform. Fixes NoSuchMethodError on StaticJavaParser.parseJavadoc(String) in Micronaut test-example projects after the 8.0.x bump to javaparser-core 3.28.0

Known Follow-up Items

  • build-logic/docs-core/ExtractDependenciesTask.groovy still uses Spring DM's shaded Maven model classes (io.spring.gradle.dependencymanagement.org.apache.maven.model.Model) for POM reading at build time. This should be migrated to JDK XML parsing in a separate follow-up.
  • Verify compatibility once the Gradle 9.3.1 upgrade PR merges (separate effort).
  • Verify the Forge/CLI project generators don't include Spring DM in generated projects.

Motivation

Standardizing on Gradle platforms is the direction the Gradle ecosystem is heading. The Spring DM plugin, while feature-rich, introduces a parallel dependency resolution system that can conflict with Gradle's native behavior. By removing it and adding only the minimal property-override bridge, Grails 8 gets:

  1. Simpler dependency resolution - one system (Gradle) instead of two (Gradle + Spring DM)
  2. Better Gradle compatibility - no more Spring DM vs Gradle resolution conflicts
  3. Preserved developer experience - ext['version.property'] and gradle.properties overrides continue to work exactly as before
  4. Smaller plugin footprint - ~350 lines of Groovy with zero external dependencies vs the full Spring DM plugin

Related

…m and lightweight BOM property overrides

Replace the Spring Dependency Management Gradle plugin with Gradle's
native platform() support plus a lightweight BomManagedVersions utility
that preserves the ability to override BOM-managed dependency versions
via project properties (ext[] or gradle.properties).

This allows Grails to standardize on Gradle platforms - the modern
dependency management solution - while retaining the one feature
Gradle platforms lack: property-based version overrides from BOMs.

Changes:
- Add BomManagedVersions: parses BOM POM XML to extract property-to-
  artifact mappings, applies version overrides via eachDependency()
- Update GrailsGradlePlugin to use platform() + BomManagedVersions
  instead of Spring DM plugin
- Deprecate GrailsExtension.springDependencyManagement flag
- Remove Spring DM plugin from plugins/build.gradle dependency
- Remove Spring DM plugin from example projects
- Update documentation to reflect Gradle platform approach
- Add unit tests (BomManagedVersionsSpec) and functional test
  (BomPlatformFunctionalSpec)

Note: build-logic/docs-core/ExtractDependenciesTask still uses Spring
DM's shaded Maven model classes and should be addressed in a follow-up.

Assisted-by: Claude Code <Claude@Claude.ai>
@jamesfredley jamesfredley self-assigned this Feb 26, 2026
@jamesfredley jamesfredley marked this pull request as ready for review February 26, 2026 16:30
Copilot AI review requested due to automatic review settings February 26, 2026 16:30
@jamesfredley jamesfredley added this to the grails:8.0.0-M1 milestone Feb 26, 2026
@jamesfredley jamesfredley added the relates-to:v8 Grails 8 label Feb 26, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the Spring Dependency Management plugin with Gradle's native platform() mechanism and introduces a lightweight utility (BomManagedVersions) to enable property-based version overrides from BOMs—the one feature Gradle platforms don't natively support.

Changes:

  • Removed Spring Dependency Management plugin dependency and usage across the codebase
  • Added BomManagedVersions utility (~350 lines) to parse BOM POMs and apply property-based version overrides
  • Updated plugin to use platform() for BOM import instead of Spring DM's mavenBom()

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/BomManagedVersions.groovy New utility class that parses BOM POMs and enables property-based version overrides via Gradle's resolution strategy
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy Replaced Spring DM plugin application with native Gradle platform support plus BomManagedVersions
grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsExtension.groovy Deprecated springDependencyManagement flag with guidance on new approach
grails-gradle/plugins/build.gradle Removed Spring DM plugin dependency
grails-test-examples/gsp-spring-boot/app/build.gradle Removed Spring DM plugin from example project
grails-data-graphql/examples/spring-boot-app/build.gradle Removed Spring DM plugin from example project
grails-doc/src/en/guide/commandLine/gradleBuild/gradleDependencies.adoc Updated documentation to reflect platform-based approach and property override mechanism
grails-profiles/plugin/templates/grailsCentralPublishing.gradle Updated comment to reflect new version management approach
grails-bom/build.gradle Updated comment about version property references
grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomManagedVersionsSpec.groovy Unit tests for BOM parsing and property extraction
grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/BomPlatformFunctionalSpec.groovy Functional test verifying platform integration
grails-gradle/plugins/src/test/resources/test-projects/bom-platform-basic/* Test project files for functional testing
grails-gradle/plugins/src/test/resources/test-poms/test-bom.pom Test BOM POM for unit tests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…h constant

Add factory.setXIncludeAware(false) for explicit XML security hardening
and extract magic number 10 to MAX_PROPERTY_INTERPOLATION_DEPTH constant.

Assisted-by: Claude Code <Claude@Claude.ai>
@jamesfredley jamesfredley force-pushed the feat/gradle-managed-version-overrides branch 2 times, most recently from eb85805 to 5643538 Compare February 26, 2026 20:42
The Spring Dependency Management plugin applied version constraints
globally to every configuration via configurations.all() and
resolutionStrategy.eachDependency(). With the switch to Gradle's native
platform(), version constraints must be added explicitly.

Apply the grails-bom platform to all declarable configurations using
configureEach, matching the previous global behavior. Non-declarable
configurations (apiElements, runtimeElements, etc.) inherit constraints
through their parent configurations. Code quality tool configurations
(checkstyle, codenarc, etc.) are excluded because platform() constraints
participate in version conflict resolution and can upgrade transitive
dependencies, breaking the tools. Also ensure the developmentOnly
configuration always exists via maybeCreate.

Assisted-by: Claude Code <Claude@Claude.ai>
@jamesfredley jamesfredley force-pushed the feat/gradle-managed-version-overrides branch from 5643538 to 5e89656 Compare February 26, 2026 20:59
@testlens-app

This comment has been minimized.

@testlens-app

This comment has been minimized.

@testlens-app

This comment has been minimized.

# Conflicts:
#	grails-data-graphql/examples/spring-boot-app/build.gradle
…atform

applyGrailsBom() adds a regular platform(grails-bom) to every declarable
configuration, including 'implementation'. Micronaut projects still need
to declare an enforcedPlatform(grails-bom) explicitly. With both present
on the same configuration, validateEnforcedBom() was failing on the first
grails-bom it encountered (the plugin-injected regular platform) before
it could check the user's enforcedPlatform declaration.

Scan all grails-bom declarations on 'implementation' and accept the
project as correctly configured if any one of them is an enforcedPlatform.
Only error when grails-bom is present but no enforcedPlatform exists.

Assisted-by: claude-code:claude-opus-4
…form

applyGrailsBom() adds a regular platform(grails-bom) to every declarable
configuration. For annotation-processor classpaths this conflicts with the
platform the user imports themselves (typically
io.micronaut.platform:micronaut-platform for Micronaut projects). Since
platform() constraints participate in Gradle version conflict resolution,
grails-bom's higher javaparser-core version wins over what Micronaut's
inject-java processor was compiled against, producing
NoSuchMethodError on StaticJavaParser.parseJavadoc(String) during
compileJava.

Exclude annotationProcessor and *AnnotationProcessor configurations using
the same mechanism that already excludes code-quality tool classpaths
(checkstyle, codenarc, pmd, spotbugs). Rename the helper to reflect the
broadened scope.

Assisted-by: claude-code:claude-opus-4
@jamesfredley jamesfredley requested a review from jdaugherty April 16, 2026 19:42
@testlens-app
Copy link
Copy Markdown

testlens-app bot commented Apr 16, 2026

✅ All tests passed ✅

🏷️ Commit: 54fbe93
▶️ Tests: 19660 executed
⚪️ Checks: 34/34 completed


Learn more about TestLens at testlens.app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Standardize on Gradle Platforms or Spring Dependency Management Gradle Plugin for grails-bom application

2 participants